# -*- coding: utf-8 -*-

# Copyright (c) 2021 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a checker for import statements.
"""

import ast
import re

from CodeStyleTopicChecker import CodeStyleTopicChecker


class NameOrderChecker(CodeStyleTopicChecker):
    """
    Class implementing a checker for name ordering.

    Note: Name ordering is checked for import statements, the '__all__' statement
    and exception names of exception handlers.
    """

    Codes = [
        ## Imports order
        "NO-101",
        "NO-102",
        "NO-103",
        "NO-104",
        "NO-105",
    ]
    Category = "NO"

    def __init__(self, source, filename, tree, select, ignore, expected, repeat, args):
        """
        Constructor

        @param source source code to be checked
        @type list of str
        @param filename name of the source file
        @type str
        @param tree AST tree of the source code
        @type ast.Module
        @param select list of selected codes
        @type list of str
        @param ignore list of codes to be ignored
        @type list of str
        @param expected list of expected codes
        @type list of str
        @param repeat flag indicating to report each occurrence of a code
        @type bool
        @param args dictionary of arguments for the various checks
        @type dict
        """
        super().__init__(
            NameOrderChecker.Category,
            source,
            filename,
            tree,
            select,
            ignore,
            expected,
            repeat,
            args,
        )

        # parameters for import sorting
        if args["SortOrder"] == "native":
            self.__sortingFunction = sorted
        else:
            # naturally is the default sort order
            self.__sortingFunction = self.__naturally
        self.__sortCaseSensitive = args["SortCaseSensitive"]

        checkersWithCodes = [
            (self.__checkNameOrder, ("NO-101", "NO-102", "NO-103", "NO-104", "NO-105")),
        ]
        self._initializeCheckers(checkersWithCodes)

    #######################################################################
    ## Name Order
    ##
    ## adapted from: flake8-alphabetize v0.0.21
    #######################################################################

    def __checkNameOrder(self):
        """
        Private method to check the order of import statements and handled exceptions.
        """
        from .ImportNode import ImportNode

        errors = []
        imports = []
        importNodes, aListNode, eListNodes = self.__findNodes(self.tree)

        # check for an error in '__all__'
        allError = self.__findErrorInAll(aListNode)
        if allError is not None:
            errors.append(allError)

        errors.extend(self.__findExceptionListErrors(eListNodes))

        for importNode in importNodes:
            if isinstance(importNode, ast.Import) and len(importNode.names) > 1:
                # skip suck imports because its already handled by pycodestyle
                continue

            imports.append(
                ImportNode(
                    self.args.get("ApplicationPackageNames", []),
                    importNode,
                    self,
                    self.args.get("SortIgnoringStyle", False),
                    self.args.get("SortFromFirst", False),
                )
            )

        lenImports = len(imports)
        if lenImports > 0:
            p = imports[0]
            if p.error is not None:
                errors.append(p.error)

            if lenImports > 1:
                for n in imports[1:]:
                    if n.error is not None:
                        errors.append(n.error)

                    if n == p:
                        if self.args.get("CombinedAsImports", False) or (
                            not n.asImport and not p.asImport
                        ):
                            errors.append((n.node, "NO-103", str(p), str(n)))
                    elif n < p:
                        errors.append((n.node, "NO-101", str(n), str(p)))

                    p = n

        for error in errors:
            self.addErrorFromNode(error[0], error[1], *error[2:])

    def __findExceptionListNodes(self, tree):
        """
        Private method to find all exception types handled by given tree.

        @param tree reference to the ast node tree to be parsed
        @type ast.AST
        @return list of exception types
        @rtype list of ast.Name
        """
        nodes = []

        for node in ast.walk(tree):
            if isinstance(node, ast.ExceptHandler):
                nodeType = node.type
                if isinstance(nodeType, (ast.List, ast.Tuple)):
                    nodes.append(nodeType)

        return nodes

    def __findNodes(self, tree):
        """
        Private method to find all import and import from nodes of the given
        tree.

        @param tree reference to the ast node tree to be parsed
        @type ast.AST
        @return tuple containing a list of import nodes, the '__all__' node and
            exception nodes
        @rtype tuple of (ast.Import | ast.ImportFrom, ast.List | ast.Tuple,
            ast.List | ast.Tuple)
        """
        importNodes = []
        aListNode = None
        eListNodes = self.__findExceptionListNodes(tree)

        if isinstance(tree, ast.Module):
            body = tree.body

            for n in body:
                if isinstance(n, (ast.Import, ast.ImportFrom)):
                    importNodes.append(n)

                elif isinstance(n, ast.Assign):
                    for t in n.targets:
                        if isinstance(t, ast.Name) and t.id == "__all__":
                            value = n.value

                            if isinstance(value, (ast.List, ast.Tuple)):
                                aListNode = value

        return importNodes, aListNode, eListNodes

    def __findErrorInAll(self, node):
        """
        Private method to check the '__all__' node for errors.

        @param node reference to the '__all__' node
        @type ast.List or ast.Tuple
        @return tuple containing a reference to the node, an error code and the error
            arguments
        @rtype tuple of (ast.List | ast.Tuple, str, str)
        """
        if node is not None:
            actualList = []
            for el in node.elts:
                if isinstance(el, ast.Constant):
                    actualList.append(el.value)
                else:
                    # Can't handle anything that isn't a string literal
                    return None

            expectedList = self.sorted(
                actualList,
                key=lambda k: self.moduleKey(k, subImports=True),
            )
            if expectedList != actualList:
                return (node, "NO-104", ", ".join(expectedList))

        return None

    def __findExceptionListStr(self, node):
        """
        Private method to get the exception name out of an exception handler type node.

        @param node node to be treated
        @type ast.Name or ast.Attribute
        @return string containing the exception name
        @rtype str
        """
        if isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Attribute):
            return f"{self.__findExceptionListStr(node.value)}.{node.attr}"

        return ""

    def __findExceptionListErrors(self, nodes):
        """
        Private method to check the exception node for errors.

        @param nodes list of exception nodes
        @type list of ast.List or ast.Tuple
        @return DESCRIPTION
        @rtype TYPE
        """
        errors = []

        for node in nodes:
            actualList = [self.__findExceptionListStr(elt) for elt in node.elts]

            expectedList = self.sorted(actualList)
            if expectedList != actualList:
                errors.append((node, "NO-105", ", ".join(expectedList)))

        return errors

    def sorted(self, toSort, key=None, reverse=False):
        """
        Public method to sort the given list of names.

        @param toSort list of names to be sorted
        @type list of str
        @param key function to generate keys (defaults to None)
        @type function (optional)
        @param reverse flag indicating a reverse sort (defaults to False)
        @type bool (optional)
        @return sorted list of names
        @rtype list of str
        """
        return self.__sortingFunction(toSort, key=key, reverse=reverse)

    def __naturally(self, toSort, key=None, reverse=False):
        """
        Private method to sort the given list of names naturally.

        Note: Natural sorting maintains the sort order of numbers (i.e.
            [Q1, Q10, Q2] is sorted as [Q1, Q2, Q10] while the Python
            standard sort would yield [Q1, Q10, Q2].

        @param toSort list of names to be sorted
        @type list of str
        @param key function to generate keys (defaults to None)
        @type function (optional)
        @param reverse flag indicating a reverse sort (defaults to False)
        @type bool (optional)
        @return sorted list of names
        @rtype list of str
        """
        if key is None:
            keyCallback = self.__naturalKeys
        else:

            def keyCallback(text):
                return self.__naturalKeys(key(text))

        return sorted(toSort, key=keyCallback, reverse=reverse)

    def __atoi(self, text):
        """
        Private method to convert the given text to an integer number.

        @param text text to be converted
        @type str
        @return integer number
        @rtype int
        """
        return int(text) if text.isdigit() else text

    def __naturalKeys(self, text):
        """
        Private method to generate keys for natural sorting.

        @param text text to generate a key for
        @type str
        @return key for natural sorting
        @rtype list of str or int
        """
        return [self.__atoi(c) for c in re.split(r"(\d+)", text)]

    def moduleKey(self, moduleName, subImports=False):
        """
        Public method to generate a key for the given module name.

        @param moduleName module name
        @type str
        @param subImports flag indicating a sub import like in
            'from foo import bar, baz' (defaults to False)
        @type bool (optional)
        @return generated key
        @rtype str
        """
        prefix = ""

        if subImports:
            if moduleName.isupper() and len(moduleName) > 1:
                prefix = "A"
            elif moduleName[0:1].isupper():
                prefix = "B"
            else:
                prefix = "C"
        if not self.__sortCaseSensitive:
            moduleName = moduleName.lower()

        return f"{prefix}{moduleName}"
